How do the APIs report a timeout?
The function returns FALSE and sets the last error to ERROR_SEM_TIMEOUT(0x79).
Does this protocol hold for a value of 0 for dwMilliseconds as well?
Yes.
Since it's not documented we'll go with the implementation (I'm on Win 10).

Both kernelbase!WaitForDebugEvent and kernelbase!WaitForDebugEventEx APIs call the same piece of code (simply with a different arg).
In the shared piece of code the second call is made on ntdll!DbgUiWaitStateChange which itself is a simple wrapper around ntdll!NtWaitForDebugEvent.
On function return the code checks if the returned status is either STATUS_ALERTED (0x101) or STATUS_USER_APC (0xc0). If it is, it keeps calling the previous function (ntdll!DbgUiWaitStateChange), otherwise it checks if the status is an error (more precisely if the status is signed).
If it's not signed it checks that the status is STATUS_TIMEOUT (0x102). If it is, the NT Status is converted to a win32 error - from STATUS_TIMEOUT to ERROR_SEM_TIMEOUT (0x79) - and the function is exited.
Inside nt!NtWaitForDebugEvent (kernel level) we can have a good overview of what is happening with the ReactOS source code.
After a call to nt!KeWaitForSingleObject on the debug object the code checks (source) what is the status of the call. If it is STATUS_TIMEOUT (which, if I'm not mistaken should happen in case of a 0 timeout), the function bails out and return the current status, which is, as explained above, converted to ERROR_SEM_TIMEOUT in userland.
Status = KeWaitForSingleObject(&DebugObject->EventsPresent,
Executive,
PreviousMode,
Alertable,
Timeout);
if (!NT_SUCCESS(Status) ||
(Status == STATUS_TIMEOUT) ||
(Status == STATUS_ALERTED) ||
(Status == STATUS_USER_APC))
{
/* Break out the wait */
break;