The correct, standard and portable way to define via $TZ a time zone that is called UTC and is on (with 0 offset to) Universal Time all year round is with:
TZ=UTC0
That is self-contained and give the full specification for that timezone. That tells that the offset to UTC is 0 (all year round as there's no DST part specified) and the label (as reported by date +%Z for instance) is UTC.
The timezone specification as described by TZ can get more complicated as you can also embed the DST name and offset and the rules for when to change between summer and winter time. However, those rules are limited and in particular can't cover cases where the rules change change from one year to another (and in most countries, the rules have changed over time or use rules different from the first or last Sunday in some month).
That's why POSIX also specifies TZ=:something but with how something is handled left implementation defined to allow implementations to come up with a better way to define a timezone for real life zones.
On most systems, that's implemented using the ICANN's tz database (they also publish reference code to handle those).
So you'd use TZ=:Europe/London for instance for the mainland Britain timezone which covers timezone offsets and when the change for the current year and all past years in London.
In practice, Europe/London is interpreted as the path of a file relative to some zoneinfo directory (often /usr/share/zoneinfo).
In the tz database, there is also a Etc/UTC file (with Etc/Universal and Etc/Zulu links to it). Etc/GMT is defined the same way except the label is GMT all year round (with Etc/GMT-0, Etc/GMT+0, Etc/Greenwich as links).
So, you can do TZ=:Etc/UTC to get the same effect as TZ=UTC0 except that the system needs to open that Etc/UTC file to find out about the simplest of TZ rules: 0 offset to UTC all year round.
On many (most?) systems, there's a UTC -> Etc/UTC symlink, so you can also do TZ=:UTC.
TZ=UTC itself is not POSIX, but here in the absence of an offset being given, on most systems, it's interpreted as the same as TZ=:UTC (looks for the timezone definition in /path/to/zoneinfo/UTC. But if the tz database is not available, that won't work.
Also note that with date specifically, you can use the -u option to get UTC dates regardless of what $TZ contains. date -u is equivalent to TZ=UTC0 date.